Segmentacja klientów kart kredytowych za pomocą analizy skupień¶
Michał Lata (s223352), Semion Lisichik (s217519), Kamil Kluska (s223388), Igor Kucharski (s223535), Stanisław Mierzejewski (s223558)
Streszczenie¶
Celem niniejszego projektu było przeprowadzenie segmentacji klientów kart kredytowych na podstawie ich cech demograficznych oraz zachowań finansowych, z wykorzystaniem analizy skupień. Analiza została przeprowadzona na rzeczywistym zbiorze danych zawierającym informacje o użytkownikach kart, obejmującym m.in. wiek, płeć, limit kredytowy, częstotliwość korzystania z karty, saldo zadłużenia oraz historię transakcji.
Prace rozpoczęto od oczyszczenia i przygotowania danych: usunięto zmienne nieistotne, ujednolicono typy danych, przeprowadzono enkodowanie zmiennych kategorycznych oraz standaryzację cech numerycznych. W celu ułatwienia wizualizacji i redukcji wymiarowości danych wykorzystano metodę analizy głównych składowych (PCA), która pozwoliła ograniczyć liczbę zmiennych wejściowych przy zachowaniu 33% wariancji. Następnie dane otrzymane w wyniku PCA zostały użyte do stworzenia klastrów.
Do właściwej segmentacji zastosowano algorytm KMeans, dobierając optymalną liczbę klastrów na podstawie wykresu „łokcia” oraz współczynnika sylwetki. Ostatecznie wyróżniono trzy wyraźne segmenty klientów, różniące się między sobą poziomem aktywności finansowej, wartością kredytową oraz zaangażowaniem w korzystanie z usług banku. Dodatkowo, zastosowano hierarchiczny algorytm Warda, który umożliwił stworzenie dendrogramu i wizualizację struktury klastrów w postaci hierarchicznej, co pozwoliło na głębszą analizę zależności między grupami.
Uzyskane wyniki umożliwiają stworzenie profili klientów, co może zostać wykorzystane w praktyce m.in. do projektowania spersonalizowanych ofert marketingowych, zarządzania ryzykiem kredytowym oraz identyfikacji klientów zagrożonych odejściem. Projekt pokazuje, jak techniki eksploracji danych mogą wspierać decyzje biznesowe w sektorze finansowym i stanowi punkt wyjścia do dalszych, bardziej zaawansowanych analiz predykcyjnych.
Słowa kluczowe:¶
Klasteryzacja, KMeans, PCA, Algorytm Hierarchiczny Warda, Segmentacja klientów, Analiza danych, Churn
Słowniczek:¶
- Klasteryzacja - technika analizy danych polegająca na grupowaniu obiektów w takie zbiory (klastry), w których obiekty wewnatrz klastra są do siebie bardziej podobne niż do obiektów z innych klastrów. W tym projekcie zastosowano klasteryzację w celu podziału klientów kart kredytowych na grupy o podobnych zachowaniach finansowych.
- KMeans - algorytm klasteryzacji oparty na podziale danych na k grup. Celem algorytmu jest minimalizacja sumy kwadratów odległości od punktów do środków klastra.
- PCA (Principal Component Analysis) - to metoda redukcji wymiarowości, która pozwala na uproszczenie dużych zbiorów danych poprzez przekształcenie ich do nowego układu współrzędnych, gdzie pierwsze składowe (wymiary) mają największą wariancję.
- Algorytm hierarchiczny Warda – Metoda klasteryzacji, która łączy obiekty w hierarchiczny sposób, minimalizując różnice w obrębie grup. Jest to metoda aglomeracyjna, która znajduje zastosowanie przy tworzeniu drzew hierarchicznych (dendrogramów).
- Segmentacja klientów - proces dzielenia populacji klientów na mniejsze grupy (segmenty), które mają podobne cechy.
- Analiza danych - Proces badania, oczyszczania, przekształcania i modelowania danych w celu wyciągania z nich użytecznych informacji, wniosków i wspierania podejmowania decyzji.
- Churn - Termin stosowany w marketingu, który oznacza utratę klientów lub rezygnację z usługi. W kontekście analizy danych o klientach kart kredytowych, churn oznaczaa klientów, którzy zdecydowali się zakończyć korzystanie z usług banku.
Wprowadzenie¶
Rynek kart kredytowych od dekad odgrywa istotną rolę w systemie finansowym, stanowiąc jeden z najpopularniejszych instrumentów płatniczych na świecie. Jego rozwój był ściśle powiązany z postępującą globalizacją usług finansowych, wzrostem konsumpcji oraz potrzebą zapewnienia konsumentom szybkiego i wygodnego dostępu do środków finansowych. Pierwsze karty kredytowe, jakie znamy dziś, pojawiły się w Stanach Zjednoczonych w latach 50. XX wieku, a ich gwałtowna popularyzacja nastąpiła w kolejnych dekadach, wraz z rozwojem handlu elektronicznego oraz cyfryzacją usług bankowych.
Współczesny rynek kart kredytowych to jednak nie tylko narzędzie płatnicze – to także źródło ogromnych ilości danych o zachowaniach konsumenckich. Informacje te, odpowiednio przetwarzane i analizowane, pozwalają instytucjom finansowym podejmować trafniejsze decyzje strategiczne: od oceny ryzyka kredytowego, przez personalizację oferty, aż po przeciwdziałanie odpływowi klientów (tzw. churn). W dobie dynamicznych zmian w sektorze finansowym – napędzanych m.in. przez rozwój fintechów, zmieniające się oczekiwania klientów oraz rosnącą presję na cyfryzację – umiejętność wykorzystania danych staje się kluczową przewagą konkurencyjną.
Jednym z podejść, które umożliwiają lepsze zrozumienie bazy klientów, jest segmentacja – czyli podział użytkowników na grupy o podobnych cechach lub zachowaniach. W przeciwieństwie do klasycznych metod segmentacji opartych na kryteriach demograficznych, analiza danych umożliwia tworzenie segmentów w oparciu o rzeczywiste wzorce aktywności klientów. Techniki takie jak analiza skupień (klasteryzacja) znajdują szerokie zastosowanie w marketingu, CRM (Customer Relationship Management) oraz w modelach predykcyjnych.
W niniejszym projekcie podjęto próbę segmentacji klientów kart kredytowych na podstawie dostępnych danych zawierających informacje demograficzne i behawioralne. Głównym celem analizy jest wyodrębnienie grup klientów o podobnych profilach, co może w przyszłości pomóc w personalizacji ofert, optymalizacji kampanii marketingowych oraz w strategiach utrzymania lojalnych użytkowników.
Proces analityczny rozpoczęto od wstępnego przetwarzania danych – usunięcia nieistotnych cech, konwersji zmiennych kategorycznych oraz ich normalizacji. W celu uproszczenia struktury danych i lepszego zobrazowania relacji między obserwacjami, zastosowano metodę głównych składowych (PCA) do redukcji wymiarowości. Następnie wykorzystano popularny algorytm klasteryzacji – KMeans – w celu grupowania klientów w klastry o zbliżonym profilu.
Wyniki analizy pokazują, że możliwe jest wyodrębnienie kilku znacząco różniących się grup użytkowników, m.in. klientów bardzo aktywnych, umiarkowanych oraz mało zaangażowanych. Takie podejście umożliwia nie tylko bardziej precyzyjne zarządzanie relacją z klientem, ale także wspiera identyfikację klientów o wysokim ryzyku odejścia lub tych o największym potencjale wzrostu.
Cel i zakres badania:¶
Celem badania była segmentacja klientów kart kredytowych w celu wyróżnienia grup o podobnych cechach zachowań finansowych i demograficznych. W ramach analizy:
- Zastosowano metodę PCA do redukcji wymiarowości danych,
- Użyto algorytmu KMeans do podziału klientów na klastery,
- Zastosowano hierarchiczny algorytm Warda do stworzenia dendrogramu i analizy struktury klastrów w postaci hierarchicznej, co umożliwiło głębsze zrozumienie zależności między grupami,
- Określono, które zmienne mają największy wpływ na podział klientów,
- Zbadano, jak różne segmenty klientów mogą wpływać na strategie bankowe.
Przegląd literatury¶
W swojej pracy [4] Bakoben, Bellotti i Adams wykorzystali analizę skupień do segmentacji klientów kart kredytowych na podstawie ich zachowań finansowych. Celem było zidentyfikowanie grup klientów o różnym poziomie ryzyka kredytowego poprzez analizę parametrów kont. Mancisidor, Kampffmeyer, Aas i Jenssen w swojej pracy [5] zaproponowali wykorzystanie autoenkoderów wariacyjnych (VAE) do segmentacji klientów na podstawie ukrytych klastrów. Podejście to pozwala na identyfikację segmentów klientów z różnymi profilami ryzyka kredytowego, co może wspierać procesy oceny zdolności kredytowej oraz działania marketingowe. Maciejewski natomiast w swoim artykule [3] przedstawił zastosowanie hierarchicznej analizy skupień (metoda Warda) do segmentacji użytkowników bankowości elektronicznej. Na podstawie badań przeprowadzonych na próbie 1075 osób wyodrębniono trzy segmenty: młodych entuzjastów, ostrożnych pragmatyków oraz nieufnych i nieświadomych użytkowników.
Zmienne wybrane do analizy - wraz z opisami¶
- Attrition_Flag - Określa, czy klient jest aktywny, czy odszedł od banku : ('Existing Customer' jesli klient jest aktywny, 'Attrited Customer' jesli klient odszedł)
- Customer_Age - Wiek klienta w latach
- Gender - Płeć klienta : ('M' jesli mężczyzna, 'F' jesli kobieta)
- Dependent_count - Liczba osób na utrzymaniu klienta
- Education_Level - Poziom wykształcenia klienta : ('College', 'Doctorate', 'Graduate', 'High School', 'Post-Graduate', 'Uneducated', 'Unknown')
- Marital_Status - Stan cywilny klienta : ('Divorced', 'Married', 'Single', 'Unknown')
- Income_Category - Przedział dochodowy klienta : ('Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K', '$80K - $120K', '$120K +')
- Card_Category - Typ karty kredytowej klienta : ('Blue', 'Gold', 'Platinum', 'Silver')
- Months_on_book - Liczba miesięcy, przez które klient jest związany z bankiem
- Total_Relationship_Count - Całkowita liczba produktów bankowych, z których korzysta klient :
- Months_Inactive_12_mon - Liczba miesięcy, w których klient był nieaktywny w ciągu ostatnich 12 miesięcy : (numeryczna)
- Contacts_Count_12_mon - Liczba kontaktów z bankiem w ciągu ostatnich 12 miesięcy
- Credit_Limit - Limit kredytowy na karcie klienta
- Total_Revolving_Bal - Całkowite saldo odnawialne na karcie kredytowej
- Avg_Open_To_Buy - Otwarta linia kredytowa na zakup (średnia z ostatnich 12 miesięcy)
- Total_Amt_Chng_Q4_Q1 - Zmiana w wydatkach na karcie między czwartym a pierwszym kwartałem
- Total_Trans_Amt - Całkowita kwota transakcji dokonanych przez klienta (ostatnie 12 miesięcy)
- Total_Trans_Ct - Całkowita liczba transakcji dokonanych przez klienta (ostatnie 12 miesięcy)
- Total_Ct_Chng_Q4_Q1 - Zmiana w liczbie transakcji między czwartym a pierwszym kwartałem
- Avg_Utilization_Ratio - Średni współczynnik wykorzystania karty (od 0 do 1)
Cel i zakres badania:¶
Celem badania była segmentacja klientów kart kredytowych w celu wyróżnienia grup o podobnych cechach zachowań finansowych i demograficznych. W ramach analizy:
- Zastosowano metodę PCA do redukcji wymiarowości danych,
- Użyto algorytmu KMeans do podziału klientów na klastery,
- Zastosowano hierarchiczny algorytm Warda do stworzenia dendrogramu i analizy struktury klastrów w postaci hierarchicznej, co umożliwiło głębsze zrozumienie zależności między grupami,
- Określono, które zmienne mają największy wpływ na podział klientów,
- Zbadano, jak różne segmenty klientów mogą wpływać na strategie bankowe.
Import potrzebnych bibliotek do analizy danych¶
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
Wczytanie ramki danych¶
df = pd.read_csv("BankChurners.csv")
Usuwamy niepotrzebne kolumny - pierwszą kolumnę z numerami klientów, która nie jest istotna oraz dwie ostatnie według instrukcji załączanej do ramki danych
df = df.drop(["CLIENTNUM",
"Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1",
"Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2" ],
axis=1)
Podział na zmienne kategoryczne i numeryczne¶
numerical_features = list(df.dtypes[(df.dtypes != 'object') & (df.dtypes != 'category')].index)
categorical_features = list(df.dtypes[(df.dtypes == 'object') | (df.dtypes == 'category')].index)
Obliczenie średniej, odchylenia standardowego, minimum, maksimum i wszystkich kwartyli w tym mediany¶
df.describe().transpose()
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| Customer_Age | 10127.0 | 46.325960 | 8.016814 | 26.0 | 41.000 | 46.000 | 52.000 | 73.000 |
| Dependent_count | 10127.0 | 2.346203 | 1.298908 | 0.0 | 1.000 | 2.000 | 3.000 | 5.000 |
| Months_on_book | 10127.0 | 35.928409 | 7.986416 | 13.0 | 31.000 | 36.000 | 40.000 | 56.000 |
| Total_Relationship_Count | 10127.0 | 3.812580 | 1.554408 | 1.0 | 3.000 | 4.000 | 5.000 | 6.000 |
| Months_Inactive_12_mon | 10127.0 | 2.341167 | 1.010622 | 0.0 | 2.000 | 2.000 | 3.000 | 6.000 |
| Contacts_Count_12_mon | 10127.0 | 2.455317 | 1.106225 | 0.0 | 2.000 | 2.000 | 3.000 | 6.000 |
| Credit_Limit | 10127.0 | 8631.953698 | 9088.776650 | 1438.3 | 2555.000 | 4549.000 | 11067.500 | 34516.000 |
| Total_Revolving_Bal | 10127.0 | 1162.814061 | 814.987335 | 0.0 | 359.000 | 1276.000 | 1784.000 | 2517.000 |
| Avg_Open_To_Buy | 10127.0 | 7469.139637 | 9090.685324 | 3.0 | 1324.500 | 3474.000 | 9859.000 | 34516.000 |
| Total_Amt_Chng_Q4_Q1 | 10127.0 | 0.759941 | 0.219207 | 0.0 | 0.631 | 0.736 | 0.859 | 3.397 |
| Total_Trans_Amt | 10127.0 | 4404.086304 | 3397.129254 | 510.0 | 2155.500 | 3899.000 | 4741.000 | 18484.000 |
| Total_Trans_Ct | 10127.0 | 64.858695 | 23.472570 | 10.0 | 45.000 | 67.000 | 81.000 | 139.000 |
| Total_Ct_Chng_Q4_Q1 | 10127.0 | 0.712222 | 0.238086 | 0.0 | 0.582 | 0.702 | 0.818 | 3.714 |
| Avg_Utilization_Ratio | 10127.0 | 0.274894 | 0.275691 | 0.0 | 0.023 | 0.176 | 0.503 | 0.999 |
Obliczenie skośności¶
df[numerical_features].skew()
Customer_Age -0.033605 Dependent_count -0.020826 Months_on_book -0.106565 Total_Relationship_Count -0.162452 Months_Inactive_12_mon 0.633061 Contacts_Count_12_mon 0.011006 Credit_Limit 1.666726 Total_Revolving_Bal -0.148837 Avg_Open_To_Buy 1.661697 Total_Amt_Chng_Q4_Q1 1.732063 Total_Trans_Amt 2.041003 Total_Trans_Ct 0.153673 Total_Ct_Chng_Q4_Q1 2.064031 Avg_Utilization_Ratio 0.718008 dtype: float64
Wyświetlenie typów kolumn¶
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10127 entries, 0 to 10126 Data columns (total 20 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Attrition_Flag 10127 non-null object 1 Customer_Age 10127 non-null int64 2 Gender 10127 non-null object 3 Dependent_count 10127 non-null int64 4 Education_Level 10127 non-null object 5 Marital_Status 10127 non-null object 6 Income_Category 10127 non-null object 7 Card_Category 10127 non-null object 8 Months_on_book 10127 non-null int64 9 Total_Relationship_Count 10127 non-null int64 10 Months_Inactive_12_mon 10127 non-null int64 11 Contacts_Count_12_mon 10127 non-null int64 12 Credit_Limit 10127 non-null float64 13 Total_Revolving_Bal 10127 non-null int64 14 Avg_Open_To_Buy 10127 non-null float64 15 Total_Amt_Chng_Q4_Q1 10127 non-null float64 16 Total_Trans_Amt 10127 non-null int64 17 Total_Trans_Ct 10127 non-null int64 18 Total_Ct_Chng_Q4_Q1 10127 non-null float64 19 Avg_Utilization_Ratio 10127 non-null float64 dtypes: float64(5), int64(9), object(6) memory usage: 1.5+ MB
df.isnull().sum()>0
Attrition_Flag False Customer_Age False Gender False Dependent_count False Education_Level False Marital_Status False Income_Category False Card_Category False Months_on_book False Total_Relationship_Count False Months_Inactive_12_mon False Contacts_Count_12_mon False Credit_Limit False Total_Revolving_Bal False Avg_Open_To_Buy False Total_Amt_Chng_Q4_Q1 False Total_Trans_Amt False Total_Trans_Ct False Total_Ct_Chng_Q4_Q1 False Avg_Utilization_Ratio False dtype: bool
Obsłużenie braków danych¶
Powyżej widać, że nie mamy braków danych, zatem nie musimy ich obsługiwać.
Wykresy przed obsłużeniem outliers¶
Histogramy¶
small_percent = ['Dependent_count', 'Total_Relationship_Count', 'Months_Inactive_12_mon', 'Contacts_Count_12_mon']
df.drop(small_percent, axis=1).hist(bins=16, figsize=(15, 6), layout=(3, 5))
plt.tight_layout()
plt.show()
Wykresy słupkowe¶
fig, axs = plt.subplots(1, 4, figsize=(16, 4))
for i, col in enumerate(small_percent):
percent_distribution = (df[col].value_counts(normalize=True) * 100).sort_index()
axs[i].bar(percent_distribution.index, percent_distribution.values)
axs[i].set_xlabel(col)
axs[i].set_ylabel("Procent")
axs[i].grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(16, 12))
for ax, col in zip(axes.flatten(), categorical_features):
percent = (df[col].value_counts()/len(df[col])*100).round(2)
sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
ax.set_ylabel('Procent')
ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
for p in ax.patches:
ax.annotate(f'{p.get_height():.2f}',
(p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='bottom')
fig.subplots_adjust(wspace=0.3, hspace=0.7)
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90) C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90) C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90) C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90) C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90) C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\90724676.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
Wykresy pudełkowe - przed obsłużeniem outliers¶
df.plot.box(subplots=True, layout=(5, 3), figsize=(15, 15))
plt.tight_layout()
plt.show()
Obsługa outliers¶
Obliczamy procent outliers dla każdej kolumny¶
iqr = df[numerical_features].quantile(0.75) - df[numerical_features].quantile(0.25)
was_1 = df[numerical_features].quantile(0.25) - 1.5 * iqr
was_2 = df[numerical_features].quantile(0.75) + 1.5 * iqr
iqr
Customer_Age 11.000 Dependent_count 2.000 Months_on_book 9.000 Total_Relationship_Count 2.000 Months_Inactive_12_mon 1.000 Contacts_Count_12_mon 1.000 Credit_Limit 8512.500 Total_Revolving_Bal 1425.000 Avg_Open_To_Buy 8534.500 Total_Amt_Chng_Q4_Q1 0.228 Total_Trans_Amt 2585.500 Total_Trans_Ct 36.000 Total_Ct_Chng_Q4_Q1 0.236 Avg_Utilization_Ratio 0.480 dtype: float64
outliers_per_column = ((df[numerical_features] < was_1) | (df[numerical_features] > was_2)).sum()
outliers_percentage = (outliers_per_column / len(df)) * 100
print("\nProcent outlierów w kolumnach:")
print(outliers_percentage)
Procent outlierów w kolumnach: Customer_Age 0.019749 Dependent_count 0.000000 Months_on_book 3.811593 Total_Relationship_Count 0.000000 Months_Inactive_12_mon 3.268490 Contacts_Count_12_mon 6.211119 Credit_Limit 9.716599 Total_Revolving_Bal 0.000000 Avg_Open_To_Buy 9.509233 Total_Amt_Chng_Q4_Q1 3.910339 Total_Trans_Amt 8.847635 Total_Trans_Ct 0.019749 Total_Ct_Chng_Q4_Q1 3.890590 Avg_Utilization_Ratio 0.000000 dtype: float64
Dla outliers w kolumnach w których ich procent jest mniejszy niż 5 usuwamy je, z kolei tam gdzie ich procent jest większy równy niż 5 zastępujemy ich wartości wąsami.
low_cols = outliers_percentage[outliers_percentage < 5].index.tolist()
high_cols = outliers_percentage[outliers_percentage >= 5].index.tolist()
df = df[~((df[low_cols] < was_1[low_cols]) | (df[low_cols] > was_2[low_cols])).any(axis = 1)]
df.loc[:, high_cols] = df[high_cols].clip(
lower=was_1[high_cols],
upper=was_2[high_cols],
axis=1
)
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\1237488139.py:1: FutureWarning: Setting an item of incompatible dtype is deprecated and will raise in a future error of pandas. Value '[2. 2. 2. ... 4. 3. 4.]' has dtype incompatible with int64, please explicitly cast to a compatible dtype first. df.loc[:, high_cols] = df[high_cols].clip( C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\1237488139.py:1: FutureWarning: Setting an item of incompatible dtype is deprecated and will raise in a future error of pandas. Value '[1201. 1570. 1207. ... 8619.25 8395. 8619.25]' has dtype incompatible with int64, please explicitly cast to a compatible dtype first. df.loc[:, high_cols] = df[high_cols].clip(
Po obsłużeniu outliers¶
Wykresy pudełkowe¶
df.plot.box(subplots=True, layout=(5, 3), figsize=(15, 15))
plt.tight_layout()
plt.show()
Histogramy¶
df.drop(['Dependent_count', 'Total_Relationship_Count', 'Months_Inactive_12_mon', 'Contacts_Count_12_mon'], axis=1).hist(bins=16, figsize=(15, 6), layout=(3, 5))
plt.tight_layout()
plt.show()
Wykresy słupkowe¶
fig, axs = plt.subplots(1, 4, figsize=(16, 4))
for i, col in enumerate(small_percent):
percent_distribution = (df[col].value_counts(normalize=True) * 100).sort_index()
axs[i].bar(percent_distribution.index, percent_distribution.values)
axs[i].set_xlabel(col)
axs[i].set_ylabel("Procent")
axs[i].grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
Obliczenie korelacji¶
df[numerical_features].corr()
| Customer_Age | Dependent_count | Months_on_book | Total_Relationship_Count | Months_Inactive_12_mon | Contacts_Count_12_mon | Credit_Limit | Total_Revolving_Bal | Avg_Open_To_Buy | Total_Amt_Chng_Q4_Q1 | Total_Trans_Amt | Total_Trans_Ct | Total_Ct_Chng_Q4_Q1 | Avg_Utilization_Ratio | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Customer_Age | 1.000000 | -0.116512 | 0.746226 | -0.009817 | 0.040425 | -0.010358 | 0.001680 | 0.020414 | -0.000647 | -0.078913 | -0.034566 | -0.060922 | -0.045330 | 0.018804 |
| Dependent_count | -0.116512 | 1.000000 | -0.103328 | -0.035611 | -0.018234 | -0.029568 | 0.060118 | -0.000713 | 0.060336 | -0.023812 | 0.034102 | 0.031549 | 0.001550 | -0.032719 |
| Months_on_book | 0.746226 | -0.103328 | 1.000000 | -0.009697 | 0.054766 | -0.002067 | 0.006452 | 0.012822 | 0.004718 | -0.054043 | -0.023737 | -0.039594 | -0.035614 | 0.004314 |
| Total_Relationship_Count | -0.009817 | -0.035611 | -0.009697 | 1.000000 | 0.000221 | 0.074364 | -0.061464 | 0.001799 | -0.062175 | 0.008415 | -0.366008 | -0.249780 | 0.006603 | 0.058121 |
| Months_Inactive_12_mon | 0.040425 | -0.018234 | 0.054766 | 0.000221 | 1.000000 | 0.041486 | -0.020587 | -0.052920 | -0.015413 | -0.010094 | -0.058666 | -0.083355 | -0.048004 | -0.019011 |
| Contacts_Count_12_mon | -0.010358 | -0.029568 | -0.002067 | 0.074364 | 0.041486 | 1.000000 | 0.016318 | -0.053271 | 0.021637 | -0.024042 | -0.157194 | -0.167681 | -0.102249 | -0.052915 |
| Credit_Limit | 0.001680 | 0.060118 | 0.006452 | -0.061464 | -0.020587 | 0.016318 | 1.000000 | 0.053896 | 0.994376 | 0.009957 | 0.147594 | 0.071717 | -0.017888 | -0.515554 |
| Total_Revolving_Bal | 0.020414 | -0.000713 | 0.012822 | 0.001799 | -0.052920 | -0.053271 | 0.053896 | 1.000000 | -0.046915 | 0.019757 | 0.067674 | 0.074571 | 0.076437 | 0.621056 |
| Avg_Open_To_Buy | -0.000647 | 0.060336 | 0.004718 | -0.062175 | -0.015413 | 0.021637 | 0.994376 | -0.046915 | 1.000000 | 0.007757 | 0.142089 | 0.065296 | -0.025783 | -0.584075 |
| Total_Amt_Chng_Q4_Q1 | -0.078913 | -0.023812 | -0.054043 | 0.008415 | -0.010094 | -0.024042 | 0.009957 | 0.019757 | 0.007757 | 1.000000 | 0.178025 | 0.141140 | 0.268157 | 0.013656 |
| Total_Trans_Amt | -0.034566 | 0.034102 | -0.023737 | -0.366008 | -0.058666 | -0.157194 | 0.147594 | 0.067674 | 0.142089 | 0.178025 | 1.000000 | 0.855930 | 0.245666 | -0.049074 |
| Total_Trans_Ct | -0.060922 | 0.031549 | -0.039594 | -0.249780 | -0.083355 | -0.167681 | 0.071717 | 0.074571 | 0.065296 | 0.141140 | 0.855930 | 1.000000 | 0.299843 | 0.016773 |
| Total_Ct_Chng_Q4_Q1 | -0.045330 | 0.001550 | -0.035614 | 0.006603 | -0.048004 | -0.102249 | -0.017888 | 0.076437 | -0.025783 | 0.268157 | 0.245666 | 0.299843 | 1.000000 | 0.086487 |
| Avg_Utilization_Ratio | 0.018804 | -0.032719 | 0.004314 | 0.058121 | -0.019011 | -0.052915 | -0.515554 | 0.621056 | -0.584075 | 0.013656 | -0.049074 | 0.016773 | 0.086487 | 1.000000 |
Generacja heat mapy korelacji¶
corr = df[numerical_features].corr()
plt.figure(figsize=(12, 10))
sns.heatmap(
corr,
annot=True,
fmt=".2f",
cmap='viridis',
linewidths=.5,
cbar_kws={"shrink": .8}
)
plt.xticks(rotation=90)
plt.yticks(rotation=0)
plt.title("Heatmapa korelacji", pad=20)
plt.show()
sns.pairplot(df)
<seaborn.axisgrid.PairGrid at 0x288a611de80>
Analiza skupień¶
df.sample(5)
| Attrition_Flag | Customer_Age | Gender | Dependent_count | Education_Level | Marital_Status | Income_Category | Card_Category | Months_on_book | Total_Relationship_Count | Months_Inactive_12_mon | Contacts_Count_12_mon | Credit_Limit | Total_Revolving_Bal | Avg_Open_To_Buy | Total_Amt_Chng_Q4_Q1 | Total_Trans_Amt | Total_Trans_Ct | Total_Ct_Chng_Q4_Q1 | Avg_Utilization_Ratio | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 7779 | Existing Customer | 53 | M | 4 | College | Single | $60K - $80K | Blue | 42 | 3 | 3 | 3.0 | 3096.00 | 0 | 3096.00 | 0.684 | 4709.0 | 87 | 0.776 | 0.000 |
| 3693 | Attrited Customer | 38 | F | 2 | Graduate | Single | Unknown | Blue | 24 | 3 | 2 | 3.0 | 2214.00 | 570 | 1644.00 | 0.688 | 2066.0 | 45 | 0.250 | 0.257 |
| 7728 | Existing Customer | 57 | F | 3 | High School | Single | Unknown | Blue | 37 | 3 | 1 | 2.0 | 3052.00 | 1603 | 1449.00 | 0.776 | 4210.0 | 82 | 0.907 | 0.525 |
| 1815 | Existing Customer | 38 | M | 2 | Graduate | Married | $80K - $120K | Blue | 31 | 4 | 1 | 2.0 | 23836.25 | 0 | 22660.75 | 0.781 | 1886.0 | 48 | 0.500 | 0.000 |
| 6708 | Attrited Customer | 39 | F | 2 | High School | Single | Unknown | Blue | 34 | 3 | 3 | 1.0 | 2069.00 | 550 | 1519.00 | 0.783 | 2716.0 | 43 | 0.433 | 0.266 |
Zmienne wybrane do analizy¶
Na podstawie heatmapy reprezentującej korelacje pomiędzy każdą zmienną zostały wybrane następujące zmienne:
useful_features = [
"Customer_Age",
#'Gender',
#'Education_Level',
#"Dependent_count",
"Income_Category",
#"Card_Category",
#"Months_on_book",
"Total_Relationship_Count",
"Months_Inactive_12_mon",
"Contacts_Count_12_mon",
"Credit_Limit",
"Total_Revolving_Bal",
#"Avg_Open_To_Buy",
"Total_Amt_Chng_Q4_Q1",
"Total_Trans_Amt",
#"Total_Trans_Ct",
"Total_Ct_Chng_Q4_Q1",
"Avg_Utilization_Ratio"
]
plt.figure(figsize=(12, 10))
sns.heatmap(df[useful_features].corr(numeric_only=True), cmap='viridis', annot=True)
<Axes: >
X = pd.get_dummies(df[useful_features])
X.sample(5)
| Customer_Age | Total_Relationship_Count | Months_Inactive_12_mon | Contacts_Count_12_mon | Credit_Limit | Total_Revolving_Bal | Total_Amt_Chng_Q4_Q1 | Total_Trans_Amt | Total_Ct_Chng_Q4_Q1 | Avg_Utilization_Ratio | Income_Category_$120K + | Income_Category_$40K - $60K | Income_Category_$60K - $80K | Income_Category_$80K - $120K | Income_Category_Less than $40K | Income_Category_Unknown | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 8744 | 43 | 4 | 2 | 2.0 | 11362.0 | 1871 | 0.859 | 7849.0 | 0.630 | 0.165 | False | False | False | False | True | False |
| 5758 | 52 | 3 | 3 | 2.0 | 13090.0 | 0 | 0.750 | 3851.0 | 1.000 | 0.000 | False | True | False | False | False | False |
| 8482 | 50 | 2 | 2 | 2.0 | 4060.0 | 2336 | 0.943 | 5173.0 | 0.706 | 0.575 | False | False | False | False | True | False |
| 2304 | 41 | 4 | 2 | 2.0 | 2624.0 | 1319 | 0.768 | 4189.0 | 0.609 | 0.503 | False | False | True | False | False | False |
| 4144 | 43 | 1 | 3 | 4.0 | 4579.0 | 1506 | 0.443 | 1915.0 | 0.375 | 0.329 | False | False | False | False | True | False |
X.shape
(8802, 16)
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA
Standaryzacja danych¶
Przed zastosowaniem algorytmu KMeans oraz PCA niezbędna jest standaryzacja danych, ponieważ oba algorytmy są wrażliwe na skalę zmiennych. Dane standaryzuje się najczęściej za pomocą transformacji z-score:
$$ z_i = \frac{x_i - \bar{x}}{\sigma} $$Gdzie:
Gdzie:
- $z_i$ – standaryzowana wartość zmiennej,
- $x_i$ – oryginalna wartość,
- $\bar{x}$ – średnia arytmetyczna,
- $\sigma$ – odchylenie standardowe.
scaler = StandardScaler()
scaled_X = scaler.fit_transform(X)
scaled_X
array([[-0.57662619, 0.77665931, 0.85812541, ..., -0.42655886,
-0.73532078, -0.34721773],
[ 1.44439732, 0.77665931, -0.30785481, ..., -0.42655886,
1.35995068, -0.34721773],
[-0.17242149, 1.41453887, -1.47383502, ..., -0.42655886,
-0.73532078, 2.88003725],
...,
[-0.30715639, 0.77665931, 0.85812541, ..., -0.42655886,
1.35995068, -0.34721773],
[-2.19344499, 0.13877975, 0.85812541, ..., -0.42655886,
-0.73532078, -0.34721773],
[-0.44189129, 1.41453887, -0.30785481, ..., -0.42655886,
1.35995068, -0.34721773]])
scaled_X.shape
(8802, 16)
PCA – analiza głównych składowych¶
PCA (Principal Component Analysis) to technika redukcji wymiarowości, która pozwala przekształcić dane do nowej przestrzeni współrzędnych, tak aby zachować jak najwięcej wariancji oryginalnych danych.
Kluczowe kroki:¶
- Obliczenie macierzy kowariancji:
- Znalezienie wektorów własnych:
- Projekcja danych:
model_pca = PCA(n_components=3)
pc_results = model_pca.fit_transform(scaled_X)
pc_results.shape
(8802, 3)
model_pca.explained_variance_ratio_
array([0.14048076, 0.10468107, 0.0865598 ])
Wariancja wyrażana przez PCA (33%)
np.sum(model_pca.explained_variance_ratio_)
0.3317216265414057
Metoda KMeans¶
Alokacja obiektów niehierarchiczna, która minimalizuje zmienność wewnątrz powstałych skupień, jednocześnie maksymalizując zmienność między skupieniami.
Niech $C_k$ - funkcja, która przyporządkowuje obiektowi numer skupienia.
Niech $C_k^*$ - funkcja realizująca optymalny podział. Wtedy:
gdzie $\rho$ - odległość euklidesowa.
Szukanie odpowiedniego k do modelu KMeans
ssd=[]
silh_scores = []
for k in range(2, 10):
model = KMeans(n_clusters=k, random_state=1)
labels = model.fit_predict(pc_results)
score = silhouette_score(pc_results, labels)
silh_scores.append(score)
ssd.append(model.inertia_)
Elbow Method¶
Metoda pomgająca w doborze odpowiedniej liczby klastrów, na wykresie przedstawione są SSD (Sum of Squared Distances, czyli suma kwadratów odległości punktów danych od centroidów (środków) ich przypisanych klastrów) dla każdego k. Aby wybrać odpowiednią liczbę klastrów szukamy tak zwanego łokcia na wykresie.
plt.figure(figsize=(12, 10))
plt.plot(range(2, 10), ssd, 'o--')
plt.xticks(range(2, 10));
Różnica pomiędzy środkami
pd.Series(ssd).diff()
0 NaN 1 -7872.374003 2 -4853.737520 3 -2873.753385 4 -1802.146100 5 -1213.822936 6 -1399.813008 7 -907.921430 dtype: float64
pd.Series(ssd).diff().plot(kind='bar')
<Axes: >
Metoda Silhouette'a¶
Metoda Silhouette'a (sylwetki) służy do oceny jakości klasteryzacji – czyli tego, jak dobrze dane punkty zostały pogrupowane. Dla każdego punktu oblicza się tzw. współczynnik silhouette'a, który określa, jak dobrze punkt pasuje do swojego klastra w porównaniu do innych klastrów.
Krok 1: Obliczenie wartości¶
Dla danego punktu ( i ):
- Oblicz średni dystans do wszystkich innych punktów w tym samym klastrze:
gdzie:
- $C_i$ to klaster, do którego należy punkt $i$,
- $d(i, j)$ to odległość (np. euklidesowa) między punktami $i$ i $j$,
- $a(i)$ to średni dystans punktu $i$ do pozostałych punktów w swoim klastrze.
- Oblicz średni dystans punktu $i$ do punktów w najbliższym sąsiednim klastrze:
Krok 2: Obliczenie współczynnika silhouette'a¶
$$ s(i) = \frac{b(i) - a(i)}{\max\{a(i), b(i)\}} $$- $s(i) \in [-1, 1]$
- Im bliżej 1, tym lepsze przypisanie punktu do klastra
- Wartości bliskie 0 oznaczają, że punkt leży na granicy między klastrami
- Wartości ujemne sugerują, że punkt może być przypisany do niewłaściwego klastra
Krok 3: Średni współczynnik silhouette'a¶
Jako miarę jakości całej klasteryzacji można przyjąć średnią wartość $s(i)$ dla wszystkich punktów:
$$ \bar{s} = \frac{1}{n} \sum_{i=1}^{n} s(i) $$Im większa wartość $\bar{s}$, tym lepsze dopasowanie klastrów do danych.
Sprawdzanie spójności klastrów
plt.plot(range(2, 10), silh_scores, 'o')
[<matplotlib.lines.Line2D at 0x288b6982cf0>]
Na podstawie Elbow Method i wyniku Silhoutte'a wybieramy K=3
model = KMeans(n_clusters=3, random_state=1)
labels = model.fit_predict(pc_results)
X['Kmeans labels']=labels
X.corr()['Kmeans labels'].sort_values()
Credit_Limit -0.624496 Total_Revolving_Bal -0.412681 Income_Category_$80K - $120K -0.389055 Income_Category_$120K + -0.287216 Total_Trans_Amt -0.229809 Total_Ct_Chng_Q4_Q1 -0.156232 Total_Amt_Chng_Q4_Q1 -0.108352 Income_Category_$60K - $80K -0.072470 Customer_Age -0.033119 Avg_Utilization_Ratio -0.000366 Income_Category_Unknown 0.001525 Contacts_Count_12_mon 0.085112 Total_Relationship_Count 0.088077 Months_Inactive_12_mon 0.102632 Income_Category_$40K - $60K 0.126002 Income_Category_Less than $40K 0.401966 Kmeans labels 1.000000 Name: Kmeans labels, dtype: float64
Wykresy klastrów na danych z PCA
pca_df = pd.DataFrame(data=pc_results, columns=['PC1', 'PC2', 'PC3'])
pca_df.sample(2)
| PC1 | PC2 | PC3 | |
|---|---|---|---|
| 1122 | 1.466871 | -0.779399 | 1.025176 |
| 4208 | 1.859434 | 0.097030 | 0.851515 |
sns.scatterplot(data=pca_df, x='PC1', y='PC2', hue=labels, palette='viridis')
plt.legend(loc=(1.05, 0.5));
sns.scatterplot(data=pca_df, x='PC1', y='PC3', hue=labels, palette='viridis')
plt.legend(loc=(1.05, 0.5));
sns.scatterplot(data=pca_df, x='PC2', y='PC3', hue=labels, palette='viridis')
plt.legend(loc=(1.05, 0.5));
Interpretacja klastrów, KMeans¶
df['Kmeans k=3']=labels
plt.figure(figsize=(8, 6))
percent = (df['Kmeans k=3'].value_counts()/len(df['Kmeans k=3'])*100).round(2)
ax = sns.barplot(x=percent.index, y=percent, hue=percent.index)
for p in ax.containers:
ax.bar_label(p, fmt='%.2f%%', label_type='edge', fontsize=12, padding=3)
plt.ylabel('Procent')
plt.xlabel('Grupa')
Text(0.5, 0, 'Grupa')
df.corr(numeric_only=True)['Kmeans k=3'].sort_values()
Credit_Limit -0.624496 Avg_Open_To_Buy -0.578865 Total_Revolving_Bal -0.412681 Total_Trans_Amt -0.229809 Total_Trans_Ct -0.191197 Total_Ct_Chng_Q4_Q1 -0.156232 Total_Amt_Chng_Q4_Q1 -0.108352 Dependent_count -0.046843 Months_on_book -0.033707 Customer_Age -0.033119 Avg_Utilization_Ratio -0.000366 Contacts_Count_12_mon 0.085112 Total_Relationship_Count 0.088077 Months_Inactive_12_mon 0.102632 Kmeans k=3 1.000000 Name: Kmeans k=3, dtype: float64
scaled_df = pd.DataFrame(scaled_X, columns=X.drop('Kmeans labels', axis=1).columns)
scaled_df['Kmeans k=3'] = labels
Wykresy średnich wartości po standaryzacji
fig, axes = plt.subplots(3, 1, figsize=(8, 16))
for i, ax in zip(range(labels.max()+1), axes):
scaled_df[scaled_df['Kmeans k=3'] == i].drop('Kmeans k=3', axis=1).mean().plot(kind='barh', title=f'Grupa {i}', ax=ax)
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels.max()+1),axes):
percent = (df[df['Kmeans k=3']==i]['Income_Category'].value_counts()/len(df[df['Kmeans k=3']==i]['Income_Category'])*100).round(2)
sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
ax.set_title(f'Grupa {i}')
ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
ax.set_ylabel('Procent')
for p in ax.patches:
ax.annotate(f'{p.get_height():.2f}',
(p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='bottom')
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2432325268.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2432325268.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2432325268.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels.max()+1),axes):
percent = (df[df['Kmeans k=3']==i]['Attrition_Flag'].value_counts()/len(df[df['Kmeans k=3']==i]['Attrition_Flag'])*100).round(2)
sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
ax.set_title(f'Grupa {i}')
ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
ax.set_ylabel('Procent')
for p in ax.patches:
ax.annotate(f'{p.get_height():.2f}',
(p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='bottom')
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3769000308.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3769000308.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3769000308.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
features = ['Total_Revolving_Bal', 'Avg_Utilization_Ratio', 'Total_Trans_Amt']
for f, ax in zip(features, axes):
sns.barplot(data=df, x='Kmeans k=3', y=f, hue='Card_Category', palette='Set1', ax=ax)
ax.legend(title='Typ karty',loc=(0.65, 0.76))
ax.set_xlabel('Grupa');
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels.max()+1),axes):
sns.barplot(data=df[df['Kmeans k=3'] == i], x='Income_Category', y='Credit_Limit', hue='Income_Category', ax=ax, order=['Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K',
'$80K - $120K','$120K +',]);
ax.set_title(f'Grupa {i}')
ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4063709792.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4063709792.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4063709792.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels.max()+1),axes):
sns.barplot(data=df[df['Kmeans k=3'] == i], x='Income_Category', y='Total_Revolving_Bal', hue='Income_Category', ax=ax, order=['Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K',
'$80K - $120K','$120K +',]);
ax.set_title(f'Grupa {i}')
ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4075273079.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4075273079.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4075273079.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
sns.scatterplot(data=df, x='Credit_Limit', y='Total_Revolving_Bal', hue='Kmeans k=3', palette='Set1', alpha=0.5)
plt.legend(loc=(1.05, 0.5));
sns.scatterplot(data=df, x='Customer_Age', y='Credit_Limit', hue='Kmeans k=3', palette='Set1', alpha=0.5)
plt.legend(title='Grupa')
<matplotlib.legend.Legend at 0x288b9847ec0>
Cechy charakterystczne dla poszczególnych grup¶
Grupa 0:
- Wysokie wartości:
- Total_Trans_Amt i Credit_Limit: sugeruje klientów z dużą aktywnością transakcyjną i sporym otwartym limitem kredytowym.
- Dochody: dominują kategorie $60K – $80K i $80K – $120K
- Niskie wartości:
- Income_Category_Less than $40K: raczej klienci o wyższych dochodach
Grupa 1:
- Wysokie wartości:
- Avg_Utilization_Ratio: grupa wyróżniająca się największym współczynnikiem wykorzystania karty
- Total_Trans_Amt: Ilość transakcji podobna do Grupy 0
- Niskie wartości:
- Credit_Limit: niskie limity
- Dochody: głównie w niższych kategoriach
Grupa 2:
- Wysokie wartości:
- Attrition_Flag: Grupa zawierająca największy procent zamkniętych kont
- Niskie wartości:
- Total_Trans_Amt, Credit_Limit, Avg_Utilization_Ratio: niska aktywność, niskie limity
- Dochody: głównie w niższych kategoriach
- Wysokie wartości ujemne (czyli poniżej średniej):
- Avg_Utilization_Ratio: bardzo niska
- Total_Revolving_Bal: nie korzysta z limitów odnawialnych lub robi to bardzo rozsądnie
Klasteryzacja hierarchiczna (metoda Warda)¶
Hierarchiczna analiza skupień (HAC – Hierarchical Agglomerative Clustering) polega na budowaniu hierarchii klastrów poprzez stopniowe łączenie obserwacji w większe grupy. W niniejszym projekcie zastosowano aglomeracyjną metodę Warda jako technikę łączenia klastrów.
Metoda Warda¶
Metoda Warda polega na takim łączeniu klastrów, aby minimalizować całkowitą wariancję wewnątrzklastrową. Przy każdym kroku łączone są te dwa klastry, których połączenie powoduje najmniejszy wzrost sumy kwadratów błędów (SSE).
Miara odległości między dwoma klastrami ( A ) i ( B ) w metodzie Warda:
$$ D(A, B) = \frac{|A||B|}{|A| + |B|} \cdot \| \mu_A - \mu_B \|^2 $$Gdzie:
- $|A|$, $|B|$ – liczba obserwacji w klastrach $A$ i $B$
- $\mu_A$, $\mu_B$ – centroidy klastrów $A$ i $B$,
- $\| \mu_A - \mu_B \|^2$ – kwadrat euklidesowej odległości między centroidami.
Metoda Warda faworyzuje tworzenie klastrów o zbliżonej liczebności i zwartej strukturze, co czyni ją dobrze dopasowaną do segmentacji klientów.
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage
silh_scores_agg = []
for k in range(2, 10):
model_agg = AgglomerativeClustering(n_clusters=k, linkage='ward')
labels_agg = model_agg.fit_predict(pc_results)
score = silhouette_score(pc_results, labels_agg)
silh_scores_agg.append(score)
Sprawdzenie metodą Silhouette'a odpowiedniej ilości klastrów
plt.plot(range(2, 10), silh_scores_agg, 'o')
[<matplotlib.lines.Line2D at 0x288b9a9ef30>]
Dendrogram¶
Jest to graficzne drzewo hierarchii klastrów, które można „przeciąć” na wybranym poziomie, aby uzyskać pożądaną liczbę klastrów. Drzewo przycinamy w miejscu, w którym dystans między połączonymi klastrami jest największy.
Sprawdzenie dendrogramem odpowiedniej ilości klastrów, sprawdzamy maksymalnie 10 klastrów
linkage_matrix = linkage(pc_results, method='ward')
dendrogram(linkage_matrix, truncate_mode='lastp', p=10)
plt.hlines(y=100, xmin=0, xmax=99, color='red', linestyle='--', label='Przycięcie dendrogramu (3 klastry)')
plt.xlabel('Indeks próbki')
plt.ylabel('Odległość')
plt.title('Dendrogram')
plt.legend(loc=(0.5, 0.8))
plt.show()
Dla 3 klastrów obie metody dają najlepszy rezultat, (Silhouette - największa wartość, Dendrogram - najdłuższe pionowe odcinki)
model_agg = AgglomerativeClustering(n_clusters=3, linkage='ward')
labels_agg = model_agg.fit_predict(pc_results)
Wykresy na PCA
sns.scatterplot(data=pca_df, x='PC1', y='PC2', hue=labels_agg, palette='viridis');
sns.scatterplot(data=pca_df, x='PC1', y='PC3', hue=labels_agg, palette='viridis');
sns.scatterplot(data=pca_df, x='PC2', y='PC3', hue=labels_agg, palette='viridis');
Interpretacja klastrów, Agglomerative Clustering (Hierarachiczne), metoda warda¶
scaled_df['Hierarchical k=3'] = labels_agg
df['Hierarchical k=3'] = labels_agg
plt.figure(figsize=(8, 6))
percent = (df['Hierarchical k=3'].value_counts()/len(df['Hierarchical k=3'])*100).round(2)
ax = sns.barplot(x=percent.index, y=percent, hue=percent.index)
for p in ax.containers:
ax.bar_label(p, fmt='%.2f%%', label_type='edge', fontsize=12, padding=3)
plt.ylabel('Procent')
plt.xlabel('Grupa')
Text(0.5, 0, 'Grupa')
df.drop('Kmeans k=3', axis=1).corr(numeric_only=True)['Hierarchical k=3'].sort_values()
Avg_Utilization_Ratio -0.713815 Total_Revolving_Bal -0.692917 Total_Relationship_Count -0.115134 Total_Ct_Chng_Q4_Q1 -0.087407 Customer_Age -0.047531 Months_on_book -0.031638 Total_Amt_Chng_Q4_Q1 -0.008462 Total_Trans_Ct -0.001349 Dependent_count 0.020986 Contacts_Count_12_mon 0.048027 Total_Trans_Amt 0.060385 Months_Inactive_12_mon 0.064087 Credit_Limit 0.190573 Avg_Open_To_Buy 0.267937 Hierarchical k=3 1.000000 Name: Hierarchical k=3, dtype: float64
Wykresy średnich wartości po standaryzacji
fig, axes = plt.subplots(3, 1, figsize=(8, 16))
for i, ax in zip(range(labels_agg.max()+1), axes):
scaled_df[scaled_df['Hierarchical k=3'] == i].drop(['Hierarchical k=3', 'Kmeans k=3'], axis=1).mean().plot(kind='barh', title=f'Grupa {i}', ax=ax)
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels_agg.max()+1),axes):
percent = (df[df['Hierarchical k=3']==i]['Income_Category'].value_counts()/len(df[df['Hierarchical k=3']==i]['Income_Category'])*100).round(2)
sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
ax.set_title(f'Grupa {i}')
ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
ax.set_ylabel('Procent')
for p in ax.patches:
ax.annotate(f'{p.get_height():.2f}',
(p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='bottom')
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2451833659.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2451833659.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\2451833659.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels_agg.max()+1),axes):
percent = (df[df['Hierarchical k=3']==i]['Attrition_Flag'].value_counts()/len(df[df['Hierarchical k=3']==i]['Attrition_Flag'])*100).round(2)
sns.barplot(x=percent.index, y=percent, ax=ax, hue=percent.index)
ax.set_title(f'Grupa {i}')
ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
ax.set_ylabel('Procent')
for p in ax.patches:
ax.annotate(f'{p.get_height():.2f}',
(p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='bottom')
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4088301012.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4088301012.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\4088301012.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
features = ['Total_Revolving_Bal', 'Avg_Utilization_Ratio', 'Total_Trans_Amt']
for f, ax in zip(features, axes):
sns.barplot(data=df, x='Hierarchical k=3', y=f, hue='Card_Category', palette='Set1', ax=ax)
ax.legend(title='Typ karty',loc=(0.65, 0.76))
ax.set_xlabel('Grupa');
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels_agg.max()+1),axes):
sns.barplot(data=df[df['Hierarchical k=3'] == i], x='Income_Category', y='Credit_Limit', hue='Income_Category', ax=ax, order=['Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K',
'$80K - $120K','$120K +',]);
ax.set_title(f'Grupa {i}')
ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3679465326.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3679465326.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\3679465326.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
for i, ax in zip(range(labels_agg.max()+1),axes):
sns.barplot(data=df[df['Hierarchical k=3'] == i], x='Income_Category', y='Total_Revolving_Bal', hue='Income_Category', ax=ax, order=['Unknown', 'Less than $40K', '$40K - $60K', '$60K - $80K',
'$80K - $120K','$120K +',]);
ax.set_title(f'Grupa {i}')
ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\917057512.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\917057512.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90); C:\Users\Kamil\AppData\Local\Temp\ipykernel_16276\917057512.py:6: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax.set_xticklabels(ax.get_xticklabels(), rotation=90);
sns.scatterplot(data=df, x='Credit_Limit', y='Total_Revolving_Bal', hue='Hierarchical k=3', palette='Set1', alpha=0.5)
plt.legend(loc=(1.05, 0.5));
plt.legend(title='Grupa');
sns.scatterplot(data=df, x='Customer_Age', y='Credit_Limit', hue='Hierarchical k=3', palette='Set1', alpha=0.5)
plt.legend(title='Grupa');
groups = {'0': 1, '1': 0, '2': 2}
maped_labels = df['Hierarchical k=3'].astype(str).map(groups)
np.sum(maped_labels==labels)/len(df)*100
81.86775732788003
Prawie 82% grup z modelu Hierarchicznego odpowiada tym z modelu KMeans
Różnice w modelu Hierarchicznym:¶
- Grupa 0, (odpowiednik 1 KMeans):
- Niewielkie różnice w limicie kredytowym
- Mniejsza ilość transakcji
- Model przypisał więcej danych do tej grupy niż KMeans (55% danych Hierarchiczny, 41% KMeans)
- Grupa 1, (odpowiednik 0 KMeans):
- Niewielkie różnice w danych
- Grupa 2, (odpowiednik 2 KMeans):
- Niewielkie różnice w Avg_Utilization_Ratio i Total_Revolving_Bal, w szczególności dla kart Platynowych
Interpretacje: Różnice są niewielkie a więc interpretacje pozostają te same co dla modelu KMeans
Cytowanie literatury¶
W swojej pracy Maciejewski [3] zaproponował wykorzystanie analizy skupień w celu segmentacji użytkowników bankowości elektronicznej. Autor zastosował algorytm Warda, który jest jednym z wariantów hierarchicznej analizy skupień, aby podzielić użytkowników na grupy o różnych profilach aktywności. Ta metodologia była inspiracją w naszym projekcie, pozwoliło nam to na podział klientów na grupy o podobnych cechach finansowych, takich jak liczba transakcji, wysokość limitu kredytowego czy saldo na karcie.
Omówienie wyników¶
Do segmentacji klientów zastosowano algorytm KMeans, który został porównany z algorytmem hierarchicznym (metoda warda), wykorzystując dane po standaryzacji oraz metodę PCA (Principal Component Analysis). Wybór trzech komponentów głównych, na których powstały oba modele, pozwolił na zredukowanie wymiarowości danych, zachowując 33% całkowitej wariancji, co umożliwiło uproszczoną, ale reprezentatywną analizę struktur w danych. Pomimo ograniczenia wariancji do 33%, klasteryzacja pozwoliła uchwycić istotne wzorce i stworzyć użyteczne segmenty klientów.
Dobór liczby klastrów:¶
Optymalną liczbę klastrów wybrano na podstawie dwóch metryk:
- Silhouette score, który ocenił spójność grup oraz ich rozdzielczość.
- Inertia (SSD), który umożliwił zastosowanie metody "łokcia" w celu wybrania optymalnej liczby klastrów (w tym przypadku 3) dla modelu KMeans.
- Dendrogram, który pomógł w wyborze liczby klastrów (również 3) dla modelu hierarchicznego.
Interpretacja grup:¶
Grupa 0 - Aktywni, zamożni klienci z dużą ilością transakcji i dostępem do większego kredytu. Prawdopodobnie kluczowi, lojalni klienci banku.
Grupa 1 – Aktywni, średniej klasy klienci z niskimi limitami ale za to z większą liczbą transakcji. Zapewne są to standardowi klienci bez większych wpływów.
Grupa 2 – Pasywni, nisko dochodowi klienci z małą liczbą transakcji i niskimi limitami. Mogą to być klienci potencjalnie odchodzący lub mniej wartościowi dla banku.
Wnioski:¶
Analiza pozwoliła na segmentację klientów w oparciu o ich zachowania finansowe. Podział na trzy grupy pomoże bankowi lepiej dostosować ofertę do potrzeb poszczególnych segmentów, np. zaoferowanie korzystniejszych warunków dla aktywnych użytkowników, a dla mniej zaangażowanych - programów lojalnościowych lub ofert zachęcających do większego korzystania z usług bankowych.
Podsumowanie¶
Projekt dotyczący segmentacji klientów kart kredytowych za pomocą analizy skupień został zrealizowany zgodnie z założeniami analitycznymi i przyniósł zadowalające rezultaty. Na podstawie rzeczywistego zbioru danych przeprowadzono wieloetapową analizę: od oczyszczenia i transformacji danych, przez redukcję wymiarowości za pomocą metody głównych składowych (PCA), aż po zastosowanie algorytmu KMeans oraz metody Warda w klastrowaniu hierarchicznym. Metody te pozwoliły na wyodrębnienie trzech znaczących segmentów klientów. Każdy z nich charakteryzował się odmiennym poziomem aktywności finansowej, profilem kredytowym oraz zaangażowaniem w korzystanie z usług.
Otrzymane wyniki są spójne z trendami i podejściami prezentowanymi w literaturze przedmiotu. W pracy Bakoben, Bellotti i Adamsa (2017) przedstawiono zastosowanie analizy skupień do identyfikacji ryzyka kredytowego na podstawie danych o zachowaniach klientów, co znajduje bezpośrednie odzwierciedlenie w naszym podejściu analitycznym. Z kolei badania Maciejewskiego (2014) pokazują, że segmentacja użytkowników bankowości elektronicznej z użyciem metod klasteryzacyjnych umożliwia lepsze dopasowanie komunikacji i oferty do zróżnicowanych grup klientów – podobnie jak w naszym przypadku, gdzie wyróżnione segmenty można przełożyć na konkretne działania marketingowe. Praca Mancisidora i in. (2018) wskazuje natomiast na możliwości pogłębionej analizy z wykorzystaniem bardziej zaawansowanych metod, takich jak autoenkodery, co sugeruje potencjalne kierunki dalszego rozwoju projektu.
Realizacja projektu udowodniła, że klasyczne metody analizy skupień, przy właściwym przygotowaniu danych, mogą dostarczyć wartościowych informacji biznesowych. Segmentacja klientów nie tylko zwiększa zrozumienie bazy użytkowników, ale również wspiera podejmowanie decyzji w zakresie retencji klientów, personalizacji ofert oraz zarządzania ryzykiem. Projekt może być rozwijany o dodatkowe metody nienadzorowanego uczenia maszynowego lub analizy predykcyjne dla poszczególnych segmentów, co stanowi naturalny kierunek dla przyszłych prac.
Bibliografia¶
[0] Scikit-learn, Scikit-learn Documentation. 2024 https://scikit-learn.org/0.21/documentation.html
[1] Pandas. Pandas Documentation. 2024 https://pandas.pydata.org/docs/
[2] Matplotlib. Matplotlib Documentation. 2024 https://matplotlib.org/stable/index.html
[3] Grzegorz Maciejewski. Zastosowanie analizy skupień w segmentacji użytkowników bankowości elektronicznej. (2018) https://dbc.wroc.pl/dlibra/publication/103881/edition/60209/
[4] Maha Bakoben, Tony Bellotti, Niall Adams. Identification of Credit Risk Based on Cluster Analysis of Account Behaviours. (2017) https://arxiv.org/abs/1706.07466
[5] Rogelio Andrade Mancisidor, Michael Kampffmeyer, Kjersti Aas, Robert Jenssen. Segment-Based Credit Scoring Using Latent Clusters in the Variational Autoencoder (2018) https://arxiv.org/abs/1806.02538
[6] Kaggle. Credit Card Customers Dataset. (2021) https://www.kaggle.com/datasets/sakshigoyal7/credit-card-customers